/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */

/* eslint-disable no-bitwise */

import { assert } from "@fluidframework/core-utils/internal";
import type { IVectorConsumer } from "@tiny-calc/nano";

import { type Handle, isHandleValid } from "./handletable.js";
import type { PermutationSegment, PermutationVector } from "./permutationvector.js";
import { ensureRange } from "./range.js";

/**
 * Used by PermutationVector to cache position -\> handle lookups.
 *
 * Perf: Possibly, this should eventually be inlined into PermutationVector itself, but
 * so far there's no measurable perf penalty for being a separate object (node 12 x64)
 */
export class HandleCache implements IVectorConsumer<Handle> {
	private handles: Handle[] = [];
	private start = 0;

	constructor(public readonly vector: PermutationVector) {}

	/**
	 * Returns the index of the given position in the 'handles' array as a Uint32.
	 * (If the position is not in the array, returns an integer greater than 'handles.length').
	 */
	private getIndex(position: number): number {
		return (position - this.start) >>> 0;
	}

	/**
	 * Returns the handle currently assigned to the given 'position' (if any).  Check
	 * the result with 'isValidHandle(..)' to see if a handle has been allocated for
	 * the given position.
	 *
	 * @throws A 'RangeError' if the provided 'position' is out-of-bounds with regards to the
	 * PermutationVector's length.
	 */
	public getHandle(position: number): Handle {
		const index = this.getIndex(position);

		// Perf: To encourage inlining, handling of the 'cacheMiss(..)' case has been extracted
		//       to a separate method.

		// Perf: A cache hit implies that 'position' was in bounds.  Therefore, we can defer
		//       checking that 'position' is in bounds until 'cacheMiss(..)'.  This yields an
		//       ~40% speedup when the position is in the cache (node v12 x64).

		return index < this.handles.length ? this.handles[index] : this.cacheMiss(position);
	}

	/**
	 * Update the cache when a handle has been allocated for a given position.
	 */
	public addHandle(position: number, handle: Handle): void {
		assert(isHandleValid(handle), 0x017 /* "Trying to add invalid handle!" */);

		const index = this.getIndex(position);
		if (index < this.handles.length) {
			assert(
				!isHandleValid(this.handles[index]),
				0x018 /* "Trying to insert handle into position with already valid handle!" */,
			);
			this.handles[index] = handle;
		}
	}

	/**
	 * Used by {@link HandleCache.cacheMiss} to retrieve handles for a range of positions.
	 * @param start - The start position (inclusive).
	 * @param end - The end position (exclusive).
	 * @param handles - The array to populate with handles. Note that it is mutated in place.
	 */
	private getHandles(start: number, end: number, handles: Handle[]): void {
		// TODO: This can be accelerated substantially using 'walkSegments()'.  The only catch
		//       is that

		const { vector } = this;

		for (let pos = start; pos < end; pos++) {
			// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
			const { segment, offset } = vector.getContainingSegment(pos)!;
			const asPerm = segment as PermutationSegment;
			handles.push(asPerm.start + offset);
		}
	}

	private cacheMiss(position: number): Handle {
		// Coercing 'position' to an Uint32 allows us to handle a negative 'position' value
		// with the same logic that handles 'position' >= length.
		const _position = position >>> 0;

		// TODO: To bound memory usage, there should be a limit on the maximum size of
		//       handle[].

		// TODO: To reduce MergeTree lookups, this code should opportunistically grow
		//       the cache to the next MergeTree segment boundary (within the limits of
		//       the handle cache).

		if (_position < this.start) {
			const handles: Handle[] = [];
			this.getHandles(_position, this.start, handles);
			handles.push(...this.handles);
			this.handles = handles;
			this.start = _position;
			return this.handles[0];
		} else {
			ensureRange(_position, this.vector.getLength());
			this.getHandles(this.start + this.handles.length, _position + 1, this.handles);
			return this.handles[this.handles.length - 1];
		}
	}

	// #region IVectorConsumer

	itemsChanged(start: number, removedCount: number, insertedCount: number): void {
		// If positions were inserted/removed, our current policy is to trim the array
		// at the beginning of the invalidate range and lazily repopulate the handles
		// on demand.
		//
		// Some alternatives to consider that preserve the previously cached handles
		// that are still valid:
		//
		//      * Eagerly populate the 'handles[]' with the newly insert values (currently guaranteed
		//        to be Handle.unallocated, so we don't even need to look them up.)
		//
		//      * Use a sentinel value or other mechanism to allow "holes" in the cache.

		const index = this.getIndex(start);
		if (index < this.handles.length) {
			this.handles.length = index;
		}
	}

	// #endregion IVectorConsumer
}
